// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
/**
 * @modifier NHN FE Development Lab <dl_javascript@nhn.com>
 */
// based on https://github.com/codemirror/CodeMirror/blob/ff04f127ba8a736b97d06c505fb85d976e3f2980/mode/markdown/markdown.js
import CodeMirror from 'codemirror';

/*eslint-disable */
CodeMirror.defineMode(
  'markdown',
  function(cmCfg, modeCfg) {
    var htmlMode = CodeMirror.getMode(cmCfg, 'text/html');
    var htmlModeMissing = htmlMode.name == 'null';

    function getMode(name) {
      if (CodeMirror.findModeByName) {
        var found = CodeMirror.findModeByName(name);
        if (found) name = found.mime || found.mimes[0];
      }
      var mode = CodeMirror.getMode(cmCfg, name);
      return mode.name == 'null' ? null : mode;
    }

    // Should characters that affect highlighting be highlighted separate?
    // Does not include characters that will be output (such as `1.` and `-` for lists)
    if (modeCfg.highlightFormatting === undefined) modeCfg.highlightFormatting = false;

    // Maximum number of nested blockquotes. Set to 0 for infinite nesting.
    // Excess `>` will emit `error` token.
    if (modeCfg.maxBlockquoteDepth === undefined) modeCfg.maxBlockquoteDepth = 0;

    // Turn on task lists? ("- [ ] " and "- [x] ")
    if (modeCfg.taskLists === undefined) modeCfg.taskLists = false;

    // Turn on strikethrough syntax
    if (modeCfg.strikethrough === undefined) modeCfg.strikethrough = false;

    if (modeCfg.emoji === undefined) modeCfg.emoji = false;

    if (modeCfg.fencedCodeBlockHighlighting === undefined)
      modeCfg.fencedCodeBlockHighlighting = true;

    if (modeCfg.xml === undefined) modeCfg.xml = true;

    // Allow token types to be overridden by user-provided token types.
    if (modeCfg.tokenTypeOverrides === undefined) modeCfg.tokenTypeOverrides = {};

    var tokenTypes = {
      header: 'header',
      code: 'comment',
      quote: 'quote',
      list1: 'variable-2',
      list2: 'variable-3',
      list3: 'keyword',
      hr: 'hr',
      image: 'image',
      imageAltText: 'image-alt-text',
      imageMarker: 'image-marker',
      formatting: 'formatting',
      linkInline: 'link',
      linkEmail: 'link',
      linkText: 'link',
      linkHref: 'string',
      em: 'em',
      strong: 'strong',
      strikethrough: 'strikethrough',
      emoji: 'builtin'
    };

    for (var tokenType in tokenTypes) {
      if (tokenTypes.hasOwnProperty(tokenType) && modeCfg.tokenTypeOverrides[tokenType]) {
        tokenTypes[tokenType] = modeCfg.tokenTypeOverrides[tokenType];
      }
    }

    var hrRE = /^([*\-_])(?:\s*\1){2,}\s*$/,
      listRE = /^(?:[*\-+]|^[0-9]+([.)]))\s+/,
      taskListRE = /^\[(x| )\](?=\s)/i, // Must follow listRE
      atxHeaderRE = modeCfg.allowAtxHeaderWithoutSpace ? /^(#+)/ : /^(#+)(?: |$)/,
      setextHeaderRE = /^ *(?:\={1,}|-{1,})\s*$/,
      textRE = /^[^#!\[\]*_\\<>` "'(~:]+/,
      fencedCodeRE = /^(~~~+|```+)[ \t]*([\w+#-]*)[^\n`]*$/,
      linkDefRE = /^\s*\[[^\]]+?\]:.*$/, // naive link-definition
      punctuation = /[!\"#$%&\'()*+,\-\.\/:;<=>?@\[\\\]^_`{|}~]/,
      expandedTab = '    '; // CommonMark specifies tab as 4 spaces

    function switchInline(stream, state, f) {
      state.f = state.inline = f;
      return f(stream, state);
    }

    function switchBlock(stream, state, f) {
      state.f = state.block = f;
      return f(stream, state);
    }

    function lineIsEmpty(line) {
      return !line || !/\S/.test(line.string);
    }

    // Blocks

    function blankLine(state) {
      // Reset linkTitle state
      state.linkTitle = false;
      state.linkHref = false;
      state.linkText = false;
      // Reset EM state
      state.em = false;
      // Reset STRONG state
      state.strong = false;
      // Reset strikethrough state
      state.strikethrough = false;
      // Reset state.quote
      state.quote = 0;
      // Reset state.indentedCode
      state.indentedCode = false;
      if (state.f == htmlBlock) {
        var exit = htmlModeMissing;
        if (!exit) {
          var inner = CodeMirror.innerMode(htmlMode, state.htmlState);
          exit =
            inner.mode.name == 'xml' &&
            inner.state.tagStart === null &&
            !inner.state.context &&
            inner.state.tokenize.isInText;
        }
        if (exit) {
          state.f = inlineNormal;
          state.block = blockNormal;
          state.htmlState = null;
        }
      }
      // Reset state.trailingSpace
      state.trailingSpace = 0;
      state.trailingSpaceNewLine = false;
      // Mark this line as blank
      state.prevLine = state.thisLine;
      state.thisLine = { stream: null };
      return null;
    }

    function blockNormal(stream, state) {
      var firstTokenOnLine = stream.column() === state.indentation;
      var prevLineLineIsEmpty = lineIsEmpty(state.prevLine.stream);
      var prevLineIsIndentedCode = state.indentedCode;
      var prevLineIsHr = state.prevLine.hr;
      var prevLineIsList = state.list !== false;
      var maxNonCodeIndentation = (state.listStack[state.listStack.length - 1] || 0) + 3;

      state.indentedCode = false;

      var lineIndentation = state.indentation;
      // compute once per line (on first token)
      if (state.indentationDiff === null) {
        state.indentationDiff = state.indentation;
        if (prevLineIsList) {
          // Reset inline styles which shouldn't propagate aross list items
          state.em = false;
          state.strong = false;
          state.code = false;
          state.strikethrough = false;

          state.list = null;
          // While this list item's marker's indentation is less than the deepest
          //  list item's content's indentation,pop the deepest list item
          //  indentation off the stack, and update block indentation state
          while (lineIndentation < state.listStack[state.listStack.length - 1]) {
            state.listStack.pop();
            if (state.listStack.length) {
              state.indentation = state.listStack[state.listStack.length - 1];
              // less than the first list's indent -> the line is no longer a list
            } else {
              state.list = false;
            }
          }
          if (state.list !== false) {
            state.indentationDiff = lineIndentation - state.listStack[state.listStack.length - 1];
          }
        }
      }

      // not comprehensive (currently only for setext detection purposes)
      var allowsInlineContinuation =
        !prevLineLineIsEmpty &&
        !prevLineIsHr &&
        !state.prevLine.header &&
        (!prevLineIsList || !prevLineIsIndentedCode) &&
        !state.prevLine.fencedCodeEnd;

      var isHr =
        (state.list === false || prevLineIsHr || prevLineLineIsEmpty) &&
        state.indentation <= maxNonCodeIndentation &&
        stream.match(hrRE);

      var match = null;
      if (
        state.indentationDiff >= 4 &&
        (prevLineIsIndentedCode ||
          state.prevLine.fencedCodeEnd ||
          state.prevLine.header ||
          prevLineLineIsEmpty)
      ) {
        stream.skipToEnd();
        state.indentedCode = true;
        return tokenTypes.code;
      } else if (stream.eatSpace()) {
        return null;
      } else if (
        firstTokenOnLine &&
        state.indentation <= maxNonCodeIndentation &&
        (match = stream.match(atxHeaderRE)) &&
        match[1].length <= 6
      ) {
        state.quote = 0;
        state.header = match[1].length;
        state.thisLine.header = true;
        if (modeCfg.highlightFormatting) state.formatting = 'header';
        state.f = state.inline;
        return getType(state);
      } else if (state.indentation <= maxNonCodeIndentation && stream.eat('>')) {
        state.quote = firstTokenOnLine ? 1 : state.quote + 1;
        if (modeCfg.highlightFormatting) state.formatting = 'quote';
        stream.eatSpace();
        return getType(state);
      } else if (
        !isHr &&
        !state.setext &&
        firstTokenOnLine &&
        state.indentation <= maxNonCodeIndentation &&
        (match = stream.match(listRE))
      ) {
        var listType = match[1] ? 'ol' : 'ul';

        state.indentation = lineIndentation + stream.current().length;
        state.list = true;
        state.quote = 0;

        // Add this list item's content's indentation to the stack
        state.listStack.push(state.indentation);

        if (modeCfg.taskLists && stream.match(taskListRE, false)) {
          state.taskList = true;
        }
        state.f = state.inline;
        if (modeCfg.highlightFormatting) state.formatting = ['list', 'list-' + listType];
        return getType(state);
      } else if (
        firstTokenOnLine &&
        state.indentation <= maxNonCodeIndentation &&
        (match = stream.match(fencedCodeRE, true))
      ) {
        state.quote = 0;
        state.fencedEndRE = new RegExp(match[1] + '+ *$');
        // try switching mode
        state.localMode = modeCfg.fencedCodeBlockHighlighting && getMode(match[2]);
        if (state.localMode) state.localState = CodeMirror.startState(state.localMode);
        state.f = state.block = local;
        if (modeCfg.highlightFormatting) state.formatting = 'code-block';
        state.code = -1;
        return getType(state);
        // SETEXT has lowest block-scope precedence after HR, so check it after
        //  the others (code, blockquote, list...)
      } else if (
        // if setext set, indicates line after ---/===
        state.setext ||
        // line before ---/===
        ((!allowsInlineContinuation || !prevLineIsList) &&
          !state.quote &&
          state.list === false &&
          !state.code &&
          !isHr &&
          !linkDefRE.test(stream.string) &&
          (match = stream.lookAhead(1)) &&
          (match = match.match(setextHeaderRE)))
      ) {
        if (!state.setext) {
          state.header = match[0].charAt(0) == '=' ? 1 : 2;
          state.setext = state.header;
        } else {
          state.header = state.setext;
          // has no effect on type so we can reset it now
          state.setext = 0;
          stream.skipToEnd();
          if (modeCfg.highlightFormatting) state.formatting = 'header';
        }
        state.thisLine.header = true;
        state.f = state.inline;
        return getType(state);
      } else if (isHr) {
        stream.skipToEnd();
        state.hr = true;
        state.thisLine.hr = true;
        return tokenTypes.hr;
      } else if (stream.peek() === '[') {
        return switchInline(stream, state, footnoteLink);
      }

      return switchInline(stream, state, state.inline);
    }

    function htmlBlock(stream, state) {
      var style = htmlMode.token(stream, state.htmlState);
      if (!htmlModeMissing) {
        var inner = CodeMirror.innerMode(htmlMode, state.htmlState);
        if (
          (inner.mode.name == 'xml' &&
            inner.state.tagStart === null &&
            !inner.state.context &&
            inner.state.tokenize.isInText) ||
          (state.md_inside && stream.current().indexOf('>') > -1)
        ) {
          state.f = inlineNormal;
          state.block = blockNormal;
          state.htmlState = null;
        }
      }
      return style;
    }

    function local(stream, state) {
      var currListInd = state.listStack[state.listStack.length - 1] || 0;
      var hasExitedList = state.indentation < currListInd;
      var maxFencedEndInd = currListInd + 3;
      if (
        state.fencedEndRE &&
        state.indentation <= maxFencedEndInd &&
        (hasExitedList || stream.match(state.fencedEndRE))
      ) {
        if (modeCfg.highlightFormatting) state.formatting = 'code-block';
        var returnType;
        if (!hasExitedList) returnType = getType(state);
        state.localMode = state.localState = null;
        state.block = blockNormal;
        state.f = inlineNormal;
        state.fencedEndRE = null;
        state.code = 0;
        state.thisLine.fencedCodeEnd = true;
        if (hasExitedList) return switchBlock(stream, state, state.block);
        return returnType;
      } else if (state.localMode) {
        return state.localMode.token(stream, state.localState);
      } else {
        stream.skipToEnd();
        return tokenTypes.code;
      }
    }

    // Inline
    function getType(state) {
      var styles = [];

      if (state.formatting) {
        styles.push(tokenTypes.formatting);

        if (typeof state.formatting === 'string') state.formatting = [state.formatting];

        for (var i = 0; i < state.formatting.length; i++) {
          styles.push(tokenTypes.formatting + '-' + state.formatting[i]);

          if (state.formatting[i] === 'header') {
            styles.push(tokenTypes.formatting + '-' + state.formatting[i] + '-' + state.header);
          }

          // Add `formatting-quote` and `formatting-quote-#` for blockquotes
          // Add `error` instead if the maximum blockquote nesting depth is passed
          if (state.formatting[i] === 'quote') {
            if (!modeCfg.maxBlockquoteDepth || modeCfg.maxBlockquoteDepth >= state.quote) {
              styles.push(tokenTypes.formatting + '-' + state.formatting[i] + '-' + state.quote);
            } else {
              styles.push('error');
            }
          }
        }
      }

      if (state.taskOpen) {
        styles.push('meta');
        return styles.length ? styles.join(' ') : null;
      }
      if (state.taskClosed) {
        styles.push('property');
        return styles.length ? styles.join(' ') : null;
      }

      if (state.linkHref) {
        styles.push(tokenTypes.linkHref, 'url');
      } else {
        // Only apply inline styles to non-url text
        if (state.strong) {
          styles.push(tokenTypes.strong);
        }
        if (state.em) {
          styles.push(tokenTypes.em);
        }
        if (state.strikethrough) {
          styles.push(tokenTypes.strikethrough);
        }
        if (state.emoji) {
          styles.push(tokenTypes.emoji);
        }
        if (state.linkText) {
          styles.push(tokenTypes.linkText);
        }
        if (state.code) {
          styles.push(tokenTypes.code);
        }
        if (state.image) {
          styles.push(tokenTypes.image);
        }
        if (state.imageAltText) {
          styles.push(tokenTypes.imageAltText, 'link');
        }
        if (state.imageMarker) {
          styles.push(tokenTypes.imageMarker);
        }
      }

      if (state.header) {
        styles.push(tokenTypes.header, tokenTypes.header + '-' + state.header);
      }

      if (state.quote) {
        styles.push(tokenTypes.quote);

        // Add `quote-#` where the maximum for `#` is modeCfg.maxBlockquoteDepth
        if (!modeCfg.maxBlockquoteDepth || modeCfg.maxBlockquoteDepth >= state.quote) {
          styles.push(tokenTypes.quote + '-' + state.quote);
        } else {
          styles.push(tokenTypes.quote + '-' + modeCfg.maxBlockquoteDepth);
        }
      }

      if (state.list !== false) {
        var listMod = (state.listStack.length - 1) % 3;
        if (!listMod) {
          styles.push(tokenTypes.list1);
        } else if (listMod === 1) {
          styles.push(tokenTypes.list2);
        } else {
          styles.push(tokenTypes.list3);
        }
      }

      if (state.trailingSpaceNewLine) {
        styles.push('trailing-space-new-line');
      } else if (state.trailingSpace) {
        styles.push('trailing-space-' + (state.trailingSpace % 2 ? 'a' : 'b'));
      }

      return styles.length ? styles.join(' ') : null;
    }

    function handleText(stream, state) {
      if (stream.match(textRE, true)) {
        return getType(state);
      }
      return undefined;
    }

    function inlineNormal(stream, state) {
      var style = state.text(stream, state);
      if (typeof style !== 'undefined') return style;

      if (state.list) {
        // List marker (*, +, -, 1., etc)
        state.list = null;
        return getType(state);
      }

      if (state.taskList) {
        var taskOpen = stream.match(taskListRE, true)[1] === ' ';
        if (taskOpen) state.taskOpen = true;
        else state.taskClosed = true;
        if (modeCfg.highlightFormatting) state.formatting = 'task';
        state.taskList = false;
        return getType(state);
      }

      state.taskOpen = false;
      state.taskClosed = false;

      if (state.header && stream.match(/^#+$/, true)) {
        if (modeCfg.highlightFormatting) state.formatting = 'header';
        return getType(state);
      }

      var ch = stream.next();

      // Matches link titles present on next line
      if (state.linkTitle) {
        state.linkTitle = false;
        var matchCh = ch;
        if (ch === '(') {
          matchCh = ')';
        }
        matchCh = (matchCh + '').replace(/([.?*+^\[\]\\(){}|-])/g, '\\$1');
        var regex = '^\\s*(?:[^' + matchCh + '\\\\]+|\\\\\\\\|\\\\.)' + matchCh;
        if (stream.match(new RegExp(regex), true)) {
          return tokenTypes.linkHref;
        }
      }

      // If this block is changed, it may need to be updated in GFM mode
      if (ch === '`') {
        var previousFormatting = state.formatting;
        if (modeCfg.highlightFormatting) state.formatting = 'code';
        stream.eatWhile('`');
        var count = stream.current().length;
        if (state.code == 0 && (!state.quote || count == 1)) {
          state.code = count;
          return getType(state);
        } else if (count == state.code) {
          // Must be exact
          var t = getType(state);
          state.code = 0;
          return t;
        } else {
          state.formatting = previousFormatting;
          return getType(state);
        }
      } else if (state.code) {
        return getType(state);
      }

      if (ch === '\\') {
        stream.next();
        if (modeCfg.highlightFormatting) {
          var type = getType(state);
          var formattingEscape = tokenTypes.formatting + '-escape';
          return type ? type + ' ' + formattingEscape : formattingEscape;
        }
      }

      if (ch === '!' && stream.match(/\[[^\]]*\] ?(?:\(|\[)/, false)) {
        state.imageMarker = true;
        state.image = true;
        if (modeCfg.highlightFormatting) state.formatting = 'image';
        return getType(state);
      }

      if (ch === '[' && state.imageMarker && stream.match(/[^\]]*\](\(.*?\)| ?\[.*?\])/, false)) {
        state.imageMarker = false;
        state.imageAltText = true;
        if (modeCfg.highlightFormatting) state.formatting = 'image';
        return getType(state);
      }

      if (ch === ']' && state.imageAltText) {
        if (modeCfg.highlightFormatting) state.formatting = 'image';
        var type = getType(state);
        state.imageAltText = false;
        state.image = false;
        state.inline = state.f = linkHref;
        return type;
      }

      if (ch === '[' && !state.image) {
        if (state.linkText && stream.match(/^.*?\]/)) return getType(state);
        state.linkText = true;
        if (modeCfg.highlightFormatting) state.formatting = 'link';
        return getType(state);
      }

      if (ch === ']' && state.linkText) {
        if (modeCfg.highlightFormatting) state.formatting = 'link';
        var type = getType(state);
        state.linkText = false;
        state.inline = state.f = stream.match(/\(.*?\)| ?\[.*?\]/, false) ? linkHref : inlineNormal;
        return type;
      }

      if (ch === '<' && stream.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/, false)) {
        state.f = state.inline = linkInline;
        if (modeCfg.highlightFormatting) state.formatting = 'link';
        var type = getType(state);
        if (type) {
          type += ' ';
        } else {
          type = '';
        }
        return type + tokenTypes.linkInline;
      }

      if (ch === '<' && stream.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/, false)) {
        state.f = state.inline = linkInline;
        if (modeCfg.highlightFormatting) state.formatting = 'link';
        var type = getType(state);
        if (type) {
          type += ' ';
        } else {
          type = '';
        }
        return type + tokenTypes.linkEmail;
      }

      if (
        modeCfg.xml &&
        ch === '<' &&
        stream.match(
          /^(!--|\?|!\[CDATA\[|[a-z][a-z0-9-]*(?:\s+[a-z_:.\-]+(?:\s*=\s*[^>]+)?)*\s*(?:>|$))/i,
          false
        )
      ) {
        var end = stream.string.indexOf('>', stream.pos);
        if (end != -1) {
          var atts = stream.string.substring(stream.start, end);
          if (/markdown\s*=\s*('|"){0,1}1('|"){0,1}/.test(atts)) state.md_inside = true;
        }
        stream.backUp(1);
        state.htmlState = CodeMirror.startState(htmlMode);
        return switchBlock(stream, state, htmlBlock);
      }

      if (modeCfg.xml && ch === '<' && stream.match(/^\/\w*?>/)) {
        state.md_inside = false;
        return 'tag';
      } else if (ch === '*' || ch === '_') {
        var len = 1,
          before = stream.pos == 1 ? ' ' : stream.string.charAt(stream.pos - 2);
        while (len < 3 && stream.eat(ch)) len++;
        var after = stream.peek() || ' ';
        // See http://spec.commonmark.org/0.27/#emphasis-and-strong-emphasis
        var leftFlanking =
          !/\s/.test(after) &&
          (!punctuation.test(after) || /\s/.test(before) || punctuation.test(before));
        var rightFlanking =
          !/\s/.test(before) &&
          (!punctuation.test(before) || /\s/.test(after) || punctuation.test(after));
        var setEm = null,
          setStrong = null;
        if (len % 2) {
          // Em
          if (
            !state.em &&
            leftFlanking &&
            (ch === '*' || !rightFlanking || punctuation.test(before))
          )
            setEm = true;
          else if (
            state.em == ch &&
            rightFlanking &&
            (ch === '*' || !leftFlanking || punctuation.test(after))
          )
            setEm = false;
        }
        if (len > 1) {
          // Strong
          if (
            !state.strong &&
            leftFlanking &&
            (ch === '*' || !rightFlanking || punctuation.test(before))
          )
            setStrong = true;
          else if (
            state.strong == ch &&
            rightFlanking &&
            (ch === '*' || !leftFlanking || punctuation.test(after))
          )
            setStrong = false;
        }
        if (setStrong != null || setEm != null) {
          if (modeCfg.highlightFormatting)
            state.formatting = setEm == null ? 'strong' : setStrong == null ? 'em' : 'strong em';
          if (setEm === true) state.em = ch;
          if (setStrong === true) state.strong = ch;
          var t = getType(state);
          if (setEm === false) state.em = false;
          if (setStrong === false) state.strong = false;
          return t;
        }
      } else if (ch === ' ') {
        if (stream.eat('*') || stream.eat('_')) {
          // Probably surrounded by spaces
          if (stream.peek() === ' ') {
            // Surrounded by spaces, ignore
            return getType(state);
          } else {
            // Not surrounded by spaces, back up pointer
            stream.backUp(1);
          }
        }
      }

      if (modeCfg.strikethrough) {
        if (ch === '~' && stream.eatWhile(ch)) {
          if (state.strikethrough) {
            // Remove strikethrough
            if (modeCfg.highlightFormatting) state.formatting = 'strikethrough';
            var t = getType(state);
            state.strikethrough = false;
            return t;
          } else if (stream.match(/^[^\s]/, false)) {
            // Add strikethrough
            state.strikethrough = true;
            if (modeCfg.highlightFormatting) state.formatting = 'strikethrough';
            return getType(state);
          }
        } else if (ch === ' ') {
          if (stream.match(/^~~/, true)) {
            // Probably surrounded by space
            if (stream.peek() === ' ') {
              // Surrounded by spaces, ignore
              return getType(state);
            } else {
              // Not surrounded by spaces, back up pointer
              stream.backUp(2);
            }
          }
        }
      }

      if (modeCfg.emoji && ch === ':' && stream.match(/^[a-z_\d+-]+:/)) {
        state.emoji = true;
        if (modeCfg.highlightFormatting) state.formatting = 'emoji';
        var retType = getType(state);
        state.emoji = false;
        return retType;
      }

      if (ch === ' ') {
        if (stream.match(/^ +$/, false)) {
          state.trailingSpace++;
        } else if (state.trailingSpace) {
          state.trailingSpaceNewLine = true;
        }
      }

      return getType(state);
    }

    function linkInline(stream, state) {
      var ch = stream.next();

      if (ch === '>') {
        state.f = state.inline = inlineNormal;
        if (modeCfg.highlightFormatting) state.formatting = 'link';
        var type = getType(state);
        if (type) {
          type += ' ';
        } else {
          type = '';
        }
        return type + tokenTypes.linkInline;
      }

      stream.match(/^[^>]+/, true);

      return tokenTypes.linkInline;
    }

    function linkHref(stream, state) {
      // Check if space, and return NULL if so (to avoid marking the space)
      if (stream.eatSpace()) {
        return null;
      }
      var ch = stream.next();
      if (ch === '(' || ch === '[') {
        state.f = state.inline = getLinkHrefInside(ch === '(' ? ')' : ']');
        if (modeCfg.highlightFormatting) state.formatting = 'link-string';
        state.linkHref = true;
        return getType(state);
      }
      return 'error';
    }

    var linkRE = {
      ')': /^(?:[^\\\(\)]|\\.|\((?:[^\\\(\)]|\\.)*\))*?(?=\))/,
      ']': /^(?:[^\\\[\]]|\\.|\[(?:[^\\\[\]]|\\.)*\])*?(?=\])/
    };

    function getLinkHrefInside(endChar) {
      return function(stream, state) {
        var ch = stream.next();

        if (ch === endChar) {
          state.f = state.inline = inlineNormal;
          if (modeCfg.highlightFormatting) state.formatting = 'link-string';
          var returnState = getType(state);
          state.linkHref = false;
          return returnState;
        }

        stream.match(linkRE[endChar]);
        state.linkHref = true;
        return getType(state);
      };
    }

    function footnoteLink(stream, state) {
      if (stream.match(/^([^\]\\]|\\.)*\]:/, false)) {
        state.f = footnoteLinkInside;
        stream.next(); // Consume [
        if (modeCfg.highlightFormatting) state.formatting = 'link';
        state.linkText = true;
        return getType(state);
      }
      return switchInline(stream, state, inlineNormal);
    }

    function footnoteLinkInside(stream, state) {
      if (stream.match(/^\]:/, true)) {
        state.f = state.inline = footnoteUrl;
        if (modeCfg.highlightFormatting) state.formatting = 'link';
        var returnType = getType(state);
        state.linkText = false;
        return returnType;
      }

      stream.match(/^([^\]\\]|\\.)+/, true);

      return tokenTypes.linkText;
    }

    function footnoteUrl(stream, state) {
      // Check if space, and return NULL if so (to avoid marking the space)
      if (stream.eatSpace()) {
        return null;
      }
      // Match URL
      stream.match(/^[^\s]+/, true);
      // Check for link title
      if (stream.peek() === undefined) {
        // End of line, set flag to check next line
        state.linkTitle = true;
      } else {
        // More content on line, check if link title
        stream.match(
          /^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?/,
          true
        );
      }
      state.f = state.inline = inlineNormal;
      return tokenTypes.linkHref + ' url';
    }

    var mode = {
      startState: function() {
        return {
          f: blockNormal,

          prevLine: { stream: null },
          thisLine: { stream: null },

          block: blockNormal,
          htmlState: null,
          indentation: 0,

          inline: inlineNormal,
          text: handleText,

          formatting: false,
          linkText: false,
          linkHref: false,
          linkTitle: false,
          code: 0,
          em: false,
          strong: false,
          header: 0,
          setext: 0,
          hr: false,
          taskList: false,
          list: false,
          listStack: [],
          quote: 0,
          trailingSpace: 0,
          trailingSpaceNewLine: false,
          strikethrough: false,
          emoji: false,
          fencedEndRE: null
        };
      },

      copyState: function(s) {
        return {
          f: s.f,

          prevLine: s.prevLine,
          thisLine: s.thisLine,

          block: s.block,
          htmlState: s.htmlState && CodeMirror.copyState(htmlMode, s.htmlState),
          indentation: s.indentation,

          localMode: s.localMode,
          localState: s.localMode ? CodeMirror.copyState(s.localMode, s.localState) : null,

          inline: s.inline,
          text: s.text,
          formatting: false,
          linkText: s.linkText,
          linkTitle: s.linkTitle,
          linkHref: s.linkHref,
          code: s.code,
          em: s.em,
          strong: s.strong,
          strikethrough: s.strikethrough,
          emoji: s.emoji,
          header: s.header,
          setext: s.setext,
          hr: s.hr,
          taskList: s.taskList,
          list: s.list,
          listStack: s.listStack.slice(0),
          quote: s.quote,
          indentedCode: s.indentedCode,
          trailingSpace: s.trailingSpace,
          trailingSpaceNewLine: s.trailingSpaceNewLine,
          md_inside: s.md_inside,
          fencedEndRE: s.fencedEndRE
        };
      },

      token: function(stream, state) {
        // Reset state.formatting
        state.formatting = false;

        if (stream != state.thisLine.stream) {
          state.header = 0;
          state.hr = false;

          if (stream.match(/^\s*$/, true)) {
            blankLine(state);
            return null;
          }

          state.prevLine = state.thisLine;
          state.thisLine = { stream: stream };

          // Reset state.taskList
          state.taskList = false;

          // Reset state.trailingSpace
          state.trailingSpace = 0;
          state.trailingSpaceNewLine = false;

          if (!state.localState) {
            state.f = state.block;
            if (state.f != htmlBlock) {
              var indentation = stream.match(/^\s*/, true)[0].replace(/\t/g, expandedTab).length;
              state.indentation = indentation;
              state.indentationDiff = null;
              if (indentation > 0) return null;
            }
          }
        }
        return state.f(stream, state);
      },

      innerMode: function(state) {
        if (state.block == htmlBlock) return { state: state.htmlState, mode: htmlMode };
        if (state.localState) return { state: state.localState, mode: state.localMode };
        return { state: state, mode: mode };
      },

      indent: function(state, textAfter, line) {
        if (state.block == htmlBlock && htmlMode.indent)
          return htmlMode.indent(state.htmlState, textAfter, line);
        if (state.localState && state.localMode.indent)
          return state.localMode.indent(state.localState, textAfter, line);
        return CodeMirror.Pass;
      },

      blankLine: blankLine,

      getType: getType,

      closeBrackets: '()[]{}\'\'""``',
      fold: 'markdown'
    };
    return mode;
  },
  'xml'
);

CodeMirror.defineMIME('text/markdown', 'markdown');

CodeMirror.defineMIME('text/x-markdown', 'markdown');
